﻿using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Media3D;
using System.Windows.Markup;
using System.Windows.Shapes;

namespace GeoPlugin
{
    public class Trackball
    {
        private FrameworkElement _zoomEventSource;
        private FrameworkElement _rotateEventSource;
        private Point _previousPosition2D = new Point(0,0);
        private Vector3D _previousPosition3D = new Vector3D(0, 0, 1);

        private Transform3DGroup _transform;
        private ScaleTransform3D _scale = new ScaleTransform3D();
        private AxisAngleRotation3D _rotation = new AxisAngleRotation3D();

        public Trackball()
        {
            _transform = new Transform3DGroup();
            _transform.Children.Add(_scale);
            _transform.Children.Add(new RotateTransform3D(_rotation));
        }

        /// <summary>
        ///     A transform to move the camera or scene to the trackball's
        ///     current orientation and scale.
        /// </summary>
        public Transform3D Transform
        {
            get { return _transform; }
        }

        #region Event Handling

        /// <summary>
        ///     The FrameworkElement we listen to for mouse events.
        /// </summary>

        public FrameworkElement RotateEventSource
        {
            get { return _rotateEventSource; }

            set
            {
                if (_rotateEventSource != null)
                {
                    _rotateEventSource.MouseRightButtonDown -= this.onmousedown;
                    _rotateEventSource.MouseRightButtonUp -= this.onmouseup;
                    _rotateEventSource.MouseMove -= this.onmousemove;
                    //_rotateEventSource.GotFocus -= this.onRotateStart;
                    //_rotateEventSource.LostFocus -= this.onRotateMove;
                }

                _rotateEventSource = value;
                _rotateEventSource.MouseRightButtonDown += this.onmousedown;
                _rotateEventSource.MouseRightButtonUp += this.onmouseup;
                _rotateEventSource.MouseMove += this.onmousemove;
                //_rotateEventSource.GotFocus += this.onRotateStart;
                //_rotateEventSource.LostFocus += this.onRotateMove;
            }
        }

        public FrameworkElement ZoomEventSource
        {
            get { return _zoomEventSource; }

            set
            {
                if (_zoomEventSource != null)
                {
                    _zoomEventSource.MouseRightButtonDown -= this.onmousedown;
                    _zoomEventSource.MouseRightButtonUp -= this.onmouseup;
                    _zoomEventSource.MouseMove -= this.onmousemove;
                    //_zoomEventSource.GotFocus -= this.onValueChanged;

                }

                _zoomEventSource = value;
                _zoomEventSource.MouseRightButtonDown += this.onmousedown;
                _zoomEventSource.MouseRightButtonUp += this.onmouseup;
                _zoomEventSource.MouseMove += this.onmousemove;
                //_zoomEventSource.GotFocus += this.onValueChanged;
            }
        }

        private void onmousedown(object sender, MouseEventArgs e)
        {
            FrameworkElement source = (FrameworkElement)sender;
            if (source.Equals(ZoomEventSource))
            {
                Mouse.Capture(ZoomEventSource, CaptureMode.Element);
                _previousPosition2D = e.GetPosition(ZoomEventSource);
                _previousPosition3D = ProjectToTrackball(
               ZoomEventSource.ActualWidth,
               ZoomEventSource.ActualHeight,
               _previousPosition2D);
            }
            else 
            {
                Mouse.Capture(RotateEventSource, CaptureMode.Element);
                _previousPosition2D = e.GetPosition(RotateEventSource);
                _previousPosition3D = ProjectToTrackball(
               RotateEventSource.ActualWidth,
               RotateEventSource.ActualHeight,
               _previousPosition2D);
            }
        }

        private void onmouseup(object sender, MouseEventArgs e)
        {
            FrameworkElement source = (FrameworkElement)sender;
            if (source.Equals(ZoomEventSource))
            { 
                Mouse.Capture(ZoomEventSource, CaptureMode.None);
            }
            else
            {
                Mouse.Capture(RotateEventSource, CaptureMode.None);
            }
        }

        private void onmousemove(object sender, MouseEventArgs e)
        {
            Point currentPosition;
            FrameworkElement source = (FrameworkElement)sender;
            if (source.Equals(ZoomEventSource))
            {
                currentPosition = e.GetPosition(ZoomEventSource);

                if (e.RightButton == MouseButtonState.Pressed)
                {
                    Zoom(currentPosition);
                }
            }
            else
            {
                currentPosition = e.GetPosition(RotateEventSource);

                if (e.RightButton == MouseButtonState.Pressed)
                {
                    Track(currentPosition);
                }
            }


            _previousPosition2D = currentPosition;
        }

        #endregion Event Handling

        private void Track(Point currentPosition)
        {
            Vector3D currentPosition3D = ProjectToTrackball(
                RotateEventSource.ActualWidth, RotateEventSource.ActualHeight, currentPosition);

            Vector3D axis = Vector3D.CrossProduct(_previousPosition3D, currentPosition3D);
            double angle = Vector3D.AngleBetween(_previousPosition3D, currentPosition3D);
            if (axis.X != 0 && axis.Y != 0 && axis.Z != 0)
            {
                Quaternion delta = new Quaternion(axis, -angle);

                // Get the current orientantion from the RotateTransform3D
                AxisAngleRotation3D r = _rotation;
                Quaternion q = new Quaternion(_rotation.Axis, _rotation.Angle);

                // Compose the delta with the previous orientation
                q *= delta;

                // Write the new orientation back to the Rotation3D
                _rotation.Axis = q.Axis;
                _rotation.Angle = q.Angle;
            }
            _previousPosition3D = currentPosition3D;
        }

        private Vector3D ProjectToTrackball(double width, double height, Point point)
        {
            double x = point.X / (width / 2);    // Scale so bounds map to [0,0] - [2,2]
            double y = point.Y / (height / 2);

            x = x - 1;                           // Translate 0,0 to the center
            y = 1 - y;                           // Flip so +Y is up instead of down

            double z2 = 1 - x * x - y * y;       // z^2 = 1 - x^2 - y^2
            double z = z2 > 0 ? Math.Sqrt(z2) : 0;

            return new Vector3D(x, y, z);
        }

        private void Zoom(Point currentPosition)
        {
            double yDelta = currentPosition.Y - _previousPosition2D.Y;

            double scale = Math.Exp(yDelta / 500);    // e^(yDelta/100) is fairly arbitrary.

            _scale.ScaleX *= scale;
            _scale.ScaleY *= scale;
            _scale.ScaleZ *= scale;
        }

    }
}
